import cv2
import numpy
from PyQt5 import QtGui
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QMainWindow, QApplication, QGraphicsScene, QFileDialog, QMessageBox
from matplotlib import pyplot as plt
from moviepy.editor import *

from mainWindow import Ui_MainWindow


# 主窗口类
class MainWindow(QMainWindow, Ui_MainWindow):
    # 类初始化
    def __init__(self):
        # 调用父类的初始化
        super(MainWindow, self).__init__()
        # 窗口界面初始化
        self.setupUi(self)

        # 当前打开的图片文件名，初始化默认空
        self.__fileName = None
        # 保存图片原始数据，初始化默认空
        self.__srcImageRGB = None
        # 保存图片最终处理结果的数据，初始化默认空
        self.__outImageRGB = None
        # 保存图片暂时修改的数据，初始化默认空
        # （在修改图像属性未点击确认时，需要暂存修改数据，如果确认后将临时数据同步为最终结果数据，如果未确认将复原数据）
        self.__tempImageRGB = None

        # 绑定窗口事件的响应函数
        # 文件菜单
        # 打开文件
        self.openFileAction.triggered.connect(self.__openFileAndShowImage)
        # 保存文件
        self.saveFileAction.triggered.connect(self.saveFile)
        # 另存为文件
        self.saveFileAsAction.triggered.connect(self.saveFileAs)
        # 退出程序
        self.exitAppAction.triggered.connect(self.close)
        # 重置图像菜单
        # 重置图像
        self.resetImageAction.triggered.connect(self.__resetImage)
        # 图像处理菜单
        # 灰度化
        self.grayAction.triggered.connect(self.__toGrayImage)
        # 二值化
        self.binaryAction.triggered.connect(self.__toBinaryImage)
        # 颜色反转
        self.reverseAction.triggered.connect(self.__reverseImage)
        # 直方图处理与显示菜单
        # 归一化直方图
        self.histogramAction.triggered.connect(self.__histogram)
        # 直方图均衡化
        self.histogramEqAction.triggered.connect(self.__histogramEqualization)
        # 空域滤波菜单
        # 均值滤波
        self.meanValueAction.triggered.connect(self.__meanValueFilter)
        # 中值滤波
        self.medianValueAction.triggered.connect(self.__medianValueFilter)
        # Sobel算子锐化
        self.sobelAction.triggered.connect(self.__sobel)
        # Prewitt算子锐化
        self.prewittAction.triggered.connect(self.__prewitt)
        # 拉普拉斯算子锐化
        self.laplacianAction.triggered.connect(self.__laplacian)
        # Canny算子
        self.cannyAction.triggered.connect(self.__canny)
        # 帮助菜单
        # 关于
        self.aboutAction.triggered.connect(self.__aboutAuthor)
        # 视频处理菜单
        # 灰度处理
        self.videoAction.triggered.connect(self.__video)
        # 摄像头人脸检测
        self.videoDetection.triggered.connect(self.__videoDetection)

    # -----------------------------------文件-----------------------------------
    # 打开文件并在主窗口中显示打开的图像
    def __openFileAndShowImage(self):
        # 打开文件选择窗口
        __fileName, _ = QFileDialog.getOpenFileName(self, '选择图片', '.', 'Image Files(*.png *.jpeg *.jpg *.bmp)')
        # 文件存在
        if __fileName and os.path.exists(__fileName):
            # 设置打开的文件名属性
            self.__fileName = __fileName
            # 转换颜色空间，cv2默认打开BGR空间，Qt界面显示需要RGB空间，所以就统一到RGB
            __bgrImg = cv2.imread(self.__fileName)
            # 设置初始化数据
            self.__srcImageRGB = cv2.cvtColor(__bgrImg, cv2.COLOR_BGR2RGB)
            self.__outImageRGB = self.__srcImageRGB.copy()
            self.__tempImageRGB = self.__srcImageRGB.copy()
            # 在窗口中左侧QGraphicsView区域显示图片
            self.__drawImage(self.srcImageView, self.__srcImageRGB)
            # 在窗口中右侧QGraphicsView区域显示图片
            self.__drawImage(self.outImageView, self.__srcImageRGB)

    # 在窗口中指定的QGraphicsView区域（左或右）显示指定类型（rgb、灰度、二值）的图像
    def __drawImage(self, location, img):
        # RBG图
        if len(img.shape) > 2:
            # 获取行（高度）、列（宽度）、通道数
            __height, __width, __channel = img.shape
            # 转换为QImage对象，注意第四、五个参数
            __qImg = QImage(img, __width, __height, __width * __channel, QImage.Format_RGB888)
        # 灰度图、二值图
        else:
            # 获取行（高度）、列（宽度）、通道数
            __height, __width = img.shape
            # 转换为QImage对象，注意第四、五个参数
            __qImg = QImage(img, __width, __height, __width, QImage.Format_Indexed8)

        # 创建QPixmap对象
        __qPixmap = QPixmap.fromImage(__qImg)
        # 创建显示容器QGraphicsScene对象
        __scene = QGraphicsScene()
        # 填充QGraphicsScene对象
        __scene.addPixmap(__qPixmap)
        # 将QGraphicsScene对象设置到QGraphicsView区域实现图片显示
        location.setScene(__scene)

    # 执行保存图片文件的操作
    def __saveImg(self, fileName):
        # 已经打开了文件才能保存
        if fileName:
            # RGB转BRG空间后才能通过opencv正确保存
            __bgrImg = cv2.cvtColor(self.__outImageRGB, cv2.COLOR_RGB2BGR)
            # 保存
            cv2.imwrite(fileName, __bgrImg)
            # 消息提示窗口
            QMessageBox.information(self, '提示', '文件保存成功！')
        else:
            # 消息提示窗口
            QMessageBox.information(self, '提示', '文件保存失败！')

    # 保存文件，覆盖原始文件
    def saveFile(self):
        self.__saveImg(self.__fileName)

    # 文件另存
    def saveFileAs(self):
        # 已经打开了文件才能保存
        if self.__fileName:
            # 打开文件保存的选择窗口
            __fileName, _ = QFileDialog.getSaveFileName(self, '保存图片', 'Image',
                                                        'Image Files(*.png *.jpeg *.jpg *.bmp)')
            self.__saveImg(__fileName)
        else:
            # 消息提示窗口
            QMessageBox.information(self, '提示', '文件保存失败！')

    # 重写窗口关闭事件函数，来关闭所有窗口。因为默认关闭主窗口子窗口依然存在。
    def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
        sys.exit(0)

    # -----------------------------------重置图片-----------------------------------
    # 重置图片到初始状态
    def __resetImage(self):
        if self.__fileName:
            # 还原文件打开时的初始化图片数据
            self.__outImageRGB = self.__srcImageRGB.copy()
            # 窗口显示图片
            self.__drawImage(self.outImageView, self.__outImageRGB)

    # -----------------------------------图像处理-----------------------------------
    # 灰度化
    def __toGrayImage(self):
        # 只有RGB图才能灰度化
        if self.__fileName and len(self.__outImageRGB.shape) > 2:
            # 灰度化使得三通道RGB图变成单通道灰度图
            self.__outImageRGB = cv2.cvtColor(self.__outImageRGB, cv2.COLOR_RGB2GRAY)
            self.__drawImage(self.outImageView, self.__outImageRGB)

    # 二值化
    def __toBinaryImage(self):
        # 先灰度化
        self.__toGrayImage()
        if self.__fileName:
            # 后阈值化为二值图
            _, self.__outImageRGB = cv2.threshold(self.__outImageRGB, 127, 255, cv2.THRESH_BINARY)
            self.__drawImage(self.outImageView, self.__outImageRGB)

    # 反转图片颜色
    def __reverseImage(self):
        if self.__fileName:
            self.__outImageRGB = cv2.bitwise_not(self.__outImageRGB)
            self.__drawImage(self.outImageView, self.__outImageRGB)

    # -----------------------------------直方图处理与显示-----------------------------------
    # 归一化直方图
    def __histogram(self):
        if self.__fileName:
            # 如果是灰度图
            if len(self.__outImageRGB.shape) < 3:
                # 使用 matplotlib 的绘图功能同时绘制单通道的直方图
                # density的类型是 bool型，指定为True,则为频率直方图，反之为频数直方图
                plt.hist(self.__outImageRGB.ravel(), bins=255, rwidth=0.8, range=(0, 256), density=True)
            # 如果是RGB图
            else:
                color = {'r', 'g', 'b'}
                # 使用 matplotlib 的绘图功能同时绘制多通道 RGB 的直方图
                for i, col in enumerate(color):
                    __hist = cv2.calcHist([self.__outImageRGB], [i], None, [256], [0, 256])
                    __hist /= self.__outImageRGB.shape[0] * self.__outImageRGB.shape[1]
                    plt.plot(__hist, color=col)
            # x轴长度区间
            plt.xlim([0, 256])
            # 显示直方图
            plt.show()

    # 直方图均衡化
    def __histogramEqualization(self):
        if self.__fileName:
            # 如果是灰度图
            if len(self.__outImageRGB.shape) < 3:
                self.__outImageRGB = cv2.equalizeHist(self.__outImageRGB)
            # 如果是RGB图
            else:
                # 分解通道，各自均衡化，再合并通道
                (r, g, b) = cv2.split(self.__outImageRGB)
                rh = cv2.equalizeHist(r)
                gh = cv2.equalizeHist(g)
                bh = cv2.equalizeHist(b)
                self.__outImageRGB = cv2.merge((rh, gh, bh))
            self.__drawImage(self.outImageView, self.__outImageRGB)

    # -----------------------------------空域滤波-----------------------------------
    # 均值滤波
    def __meanValueFilter(self):
        if self.__fileName:
            # 直接调库
            self.__outImageRGB = cv2.blur(self.__outImageRGB, (5, 5))
            self.__drawImage(self.outImageView, self.__outImageRGB)

    # 中值滤波
    def __medianValueFilter(self):
        if self.__fileName:
            # 直接调库
            self.__outImageRGB = cv2.medianBlur(self.__outImageRGB, 5)
            self.__drawImage(self.outImageView, self.__outImageRGB)

    # Sobel算子锐化
    def __sobel(self):
        if self.__fileName:
            # 直接调库
            self.__outImageRGB = cv2.Sobel(self.__outImageRGB, -1, 1, 1, 3)
            self.__drawImage(self.outImageView, self.__outImageRGB)

    # Prewitt算子锐化
    def __prewitt(self):
        if self.__fileName:
            # Prewitt 算子
            kernelx = numpy.array([[1, 1, 1], [0, 0, 0], [-1, -1, -1]], dtype=int)
            kernely = numpy.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]], dtype=int)
            # 通过自定义卷积核实现卷积
            imgx = cv2.filter2D(self.__outImageRGB, -1, kernelx)
            imgy = cv2.filter2D(self.__outImageRGB, -1, kernely)
            # 合并
            self.__outImageRGB = cv2.add(imgx, imgy)
            self.__drawImage(self.outImageView, self.__outImageRGB)

    # 拉普拉斯算子锐化
    def __laplacian(self):
        if self.__fileName:
            # 直接调库
            self.__outImageRGB = cv2.Laplacian(self.__outImageRGB, -1, ksize=3)
            self.__drawImage(self.outImageView, self.__outImageRGB)

    # Canny算子锐化
    def __canny(self):
        if self.__fileName:
            # 直接调库
            self.__outImageRGB = cv2.Canny(self.__outImageRGB, 20, 30)
            self.__drawImage(self.outImageView, self.__outImageRGB)

    # 视频处理
    def __video(self):
        if self.__fileName:
            clip = VideoFileClip(r"E:\DIPCD\DIPCD_sunshuo\image\WeChat.mp4")
            clipblackwhite = clip.fx(vfx.blackwhite)
            clipblackwhite.write_videofile(r"E:\DIPCD\DIPCD_sunshuo\image\WeChat_re.mp4")

    # 人脸检测
    def __videoDetection(self):
        if self.__fileName:
            # 创建一个级联分类器 加载一个.xml分类器文件 它既可以是Haar特征也可以是LBP特征的分类器
            faceCascade = cv2.CascadeClassifier(
                'E:\OpenCV420\opencv\sources\data\haarcascades/haarcascade_frontalface_default.xml')
            # 读取面具图像
            faceMask = cv2.imread("E:\DIPCD\DIPCD_sunshuo\image\mask.jpg")
            # 打开摄像头（视频）
            cap = cv2.VideoCapture("E:\DIPCD\DIPCD_sunshuo\image\WeChattsest.mp4")
            # 开始处理图像
            while True:
                ret, img = cap.read()
                scalingFactor = 0.75
                # 重置输入图像大小
                frame = cv2.resize(img, None, fx=scalingFactor, fy=scalingFactor, interpolation=cv2.INTER_AREA)
                # 彩色转灰度图像
                gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
                # 直方图均衡化
                frameGray = cv2.equalizeHist(gray)
                # 检测脸部，该函数在输入图像的不同尺度中检测物体
                faces = faceCascade.detectMultiScale(frameGray, 1.3, 5)

                for i in range(0, len(faces)):
                    for (x1, y1, w1, h1) in faces:
                        # 自定义参数，使面具适合脸
                        x = x1 - int(0.1 * w1)
                        y = y1 - int(0.0 * h1)
                        w = int(1.1 * w1)
                        h = int(1.1 * h1)

                    try:
                        FaceMaskSmall = cv2.resize(faceMask, (w, h), interpolation=cv2.INTER_AREA)

                        # roi确定了面具所在人脸图像上的位置
                        roi = frame[y: y + h, x: x + w]

                        # 面具图转换为灰度图
                        gray = cv2.cvtColor(FaceMaskSmall, cv2.COLOR_BGR2GRAY)

                        # 对面具图阈值分割得到mask1，也就是面具图的背景（0～50部分）
                        # 保留mask1部分的roi
                        ret, mask1 = cv2.threshold(gray, 50, 255, cv2.THRESH_BINARY_INV)
                        fg1 = cv2.bitwise_and(roi, roi, mask=mask1)

                        # 对面具图阈值分割得到面具的前景部分（50～255部分）
                        ret, mask2 = cv2.threshold(gray, 50, 255, cv2.THRESH_BINARY)
                        # 保留面具图像的面具部分
                        fg2 = cv2.bitwise_and(FaceMaskSmall, FaceMaskSmall, mask=mask2)

                        # 图像叠加
                        roi[:] = cv2.add(fg1, fg2)
                    except:
                        continue
                # 设置退出条件 展示频率
                if ord('q') == cv2.waitKey(30):
                    break
                cv2.imshow("Face Detection", frame)
            # 退出程序
            cap.release()
            cv2.destroyAllWindows()

    # -----------------------------------关于-----------------------------------
    def __aboutAuthor(self):
        QMessageBox.information(None, '帮助信息', '图像处理软件v1.0\n\n查看更多请前往README.md\n\nauthor：20101202 孙烁')


if __name__ == '__main__':
    app = QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    sys.exit(app.exec())
